Содержание

  • 1  Подготовка данных
    • 1.1  Загрузка данных
    • 1.2  Предобработка данных
      • 1.2.1  Пропуски
      • 1.2.2  Дубликаты
      • 1.2.3  Типы данных
  • 2  Оценка корректности проведения теста
    • 2.1  Есть ли пересечение с другими тестами
    • 2.2  Даты регистрации и совершения событий
    • 2.3  Есть ли пересечения между группами
    • 2.4  Регион регистрации
    • 2.5  Динамика набора пользователей в группы
    • 2.6  Пользовательская активность
    • 2.7  Вывод
  • 3  Исследовательский анализ данных
    • 3.1  Распределение количества событий на пользователя в разрезе групп
    • 3.2  Динамика количества событий в группах по дням
    • 3.3  Проверка влияния маркетинговых мероприятий
    • 3.4  Продуктовая воронка
    • 3.5  Вывод
  • 4  Оценка результатов А/В тестирования
    • 4.1  Сравнение долей
    • 4.2  Поправка на множественное тестирование
    • 4.3  Вывод
  • 5  Выводы

Оценка проведения А/В тестирования¶

Нам доступен датасет с данными пользователями, которые совершали определенные действия, а также дополнительные датасеты с подробностями. Наша задача - провести оценку результатов А/В тестирования.

Для этого нужно проверить корректность проведения теста, соответствие теста тех.заданию, а также результаты проведенного теста.

Наше техническое задание выглядит следующим образом:

  • Название теста: recommender_system_test - тестирование изменений, связанных с внедрением улучшенной рекомендательной системы;
  • Есть две группы: А - контрольная, В - новая платежная воронка (с внесенными изменениями);
  • Дата запуска теста и набора пользователей в группы: 2020-12-07;
  • Дата остановки набора новых пользователей: 2020-12-21;
  • Дата остановки теста: 2021-01-04;
  • Ожидаемое количество участников теста: 15% новых пользователей из региона EU;
  • Ожидаемый эффект: за 14 дней с момента регистрации в системе пользователи покажут улучшение каждой метрики не менее, чем на 5 процентных пунктов:
    • конверсии в просмотр карточек товаров — событие product_page,
    • просмотры корзины — product_cart,
    • покупки — purchase.

Приступим к выполнению.

Подготовка¶

К содержанию

Загрузим и обновим все необходимые библиотеки.

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import datetime as dt
import warnings
warnings.filterwarnings("ignore")
from plotly import graph_objects as go
from scipy import stats as st
import math as mth
from statsmodels.sandbox.stats.multicomp import multipletests

Подготовка данных¶

К содержанию

Загрузим данные, ознакомимся с ними и подготовим для дальнейшего исследования.

Загрузка данных¶

К содержанию

In [2]:
path = 'D://Irina//datasets//'
participants = pd.read_csv(path + 'final_ab_participants.csv')
events = pd.read_csv(path + 'final_ab_events.csv')
new_users = pd.read_csv(path + 'final_ab_new_users.csv')
marketing_events = pd.read_csv(path + 'ab_project_marketing_events.csv')

#Зададим ограничения на вывод колонок и количество символов - в данном случае "смягчим"
pd.set_option('display.max_columns', None)
pd.options.display.max_colwidth = 100

display(participants, events, new_users, marketing_events)
user_id group ab_test
0 D1ABA3E2887B6A73 A recommender_system_test
1 A7A3664BD6242119 A recommender_system_test
2 DABC14FDDFADD29E A recommender_system_test
3 04988C5DF189632E A recommender_system_test
4 482F14783456D21B B recommender_system_test
... ... ... ...
18263 1D302F8688B91781 B interface_eu_test
18264 3DE51B726983B657 A interface_eu_test
18265 F501F79D332BE86C A interface_eu_test
18266 63FBE257B05F2245 A interface_eu_test
18267 79F9ABFB029CF724 B interface_eu_test

18268 rows × 3 columns

user_id event_dt event_name details
0 E1BDDCE0DAFA2679 2020-12-07 20:22:03 purchase 99.99
1 7B6452F081F49504 2020-12-07 09:22:53 purchase 9.99
2 9CD9F34546DF254C 2020-12-07 12:59:29 purchase 4.99
3 96F27A054B191457 2020-12-07 04:02:40 purchase 4.99
4 1FD7660FDF94CA1F 2020-12-07 10:15:09 purchase 4.99
... ... ... ... ...
440312 245E85F65C358E08 2020-12-30 19:35:55 login NaN
440313 9385A108F5A0A7A7 2020-12-30 10:54:15 login NaN
440314 DB650B7559AC6EAC 2020-12-30 10:59:09 login NaN
440315 F80C9BDDEA02E53C 2020-12-30 09:53:39 login NaN
440316 7AEC61159B672CC5 2020-12-30 11:36:13 login NaN

440317 rows × 4 columns

user_id first_date region device
0 D72A72121175D8BE 2020-12-07 EU PC
1 F1C668619DFE6E65 2020-12-07 N.America Android
2 2E1BF1D4C37EA01F 2020-12-07 EU PC
3 50734A22C0C63768 2020-12-07 EU iPhone
4 E1BDDCE0DAFA2679 2020-12-07 N.America iPhone
... ... ... ... ...
61728 1DB53B933257165D 2020-12-20 EU Android
61729 538643EB4527ED03 2020-12-20 EU Mac
61730 7ADEE837D5D8CBBD 2020-12-20 EU PC
61731 1C7D23927835213F 2020-12-20 EU iPhone
61732 8F04273BB2860229 2020-12-20 EU Android

61733 rows × 4 columns

name regions start_dt finish_dt
0 Christmas&New Year Promo EU, N.America 2020-12-25 2021-01-03
1 St. Valentine's Day Giveaway EU, CIS, APAC, N.America 2020-02-14 2020-02-16
2 St. Patric's Day Promo EU, N.America 2020-03-17 2020-03-19
3 Easter Promo EU, CIS, APAC, N.America 2020-04-12 2020-04-19
4 4th of July Promo N.America 2020-07-04 2020-07-11
5 Black Friday Ads Campaign EU, CIS, APAC, N.America 2020-11-26 2020-12-01
6 Chinese New Year Promo APAC 2020-01-25 2020-02-07
7 Labor day (May 1st) Ads Campaign EU, CIS, APAC 2020-05-01 2020-05-03
8 International Women's Day Promo EU, CIS, APAC 2020-03-08 2020-03-10
9 Victory Day CIS (May 9th) Event CIS 2020-05-09 2020-05-11
10 CIS New Year Gift Lottery CIS 2020-12-30 2021-01-07
11 Dragon Boat Festival Giveaway APAC 2020-06-25 2020-07-01
12 Single's Day Gift Promo APAC 2020-11-11 2020-11-12
13 Chinese Moon Festival APAC 2020-10-01 2020-10-07

Для анализа мы получили 4 датафрейма. Посмотрим, какая информация в них хранится.

  1. participants - данные об участниках тестов:
    • user_id - идентификатор пользователя;
    • ab_test - название теста;
    • group - группа пользователя.
  2. events - все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года:
    • user_id - идентификатор пользователя;
    • event_dt - дата и время события;
    • event_name - тип события;
    • details - дополнительные данные для событий. Например, для покупок purchase, в этом поле хранится стоимость покупки в долларах.
  3. new_users - все пользователи, зарегистрировавшиеся в интернет-магазине в период с 7 по 21 декабря 2020 года:
    • user_id - идентификатор пользователя;
    • first_date - дата регистрации;
    • region - регион пользователя;
    • device - устройство, с которого происходила регистрация.
  4. marketing_events - календарь маркетинговых событий на 2020 год:
    • name - название маркетингового события;
    • regions - регионы, в которых будет проводиться рекламная кампания;
    • start_dt - дата начала кампании;
    • finish_dt - дата завершения кампании.

Теперь рассмотрим какого типа данные хранятся в наших датафреймах

In [3]:
print(participants.info())
print('='*50)
print(events.info())
print('='*50)
print(new_users.info())
print('='*50)
print(marketing_events.info())
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 18268 entries, 0 to 18267
Data columns (total 3 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   user_id  18268 non-null  object
 1   group    18268 non-null  object
 2   ab_test  18268 non-null  object
dtypes: object(3)
memory usage: 428.3+ KB
None
==================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 440317 entries, 0 to 440316
Data columns (total 4 columns):
 #   Column      Non-Null Count   Dtype  
---  ------      --------------   -----  
 0   user_id     440317 non-null  object 
 1   event_dt    440317 non-null  object 
 2   event_name  440317 non-null  object 
 3   details     62740 non-null   float64
dtypes: float64(1), object(3)
memory usage: 13.4+ MB
None
==================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 61733 entries, 0 to 61732
Data columns (total 4 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   user_id     61733 non-null  object
 1   first_date  61733 non-null  object
 2   region      61733 non-null  object
 3   device      61733 non-null  object
dtypes: object(4)
memory usage: 1.9+ MB
None
==================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 14 entries, 0 to 13
Data columns (total 4 columns):
 #   Column     Non-Null Count  Dtype 
---  ------     --------------  ----- 
 0   name       14 non-null     object
 1   regions    14 non-null     object
 2   start_dt   14 non-null     object
 3   finish_dt  14 non-null     object
dtypes: object(4)
memory usage: 580.0+ bytes
None
  • В таблице participants данные во всех колонках относятся к строчному типу, что соответствует выводимым данным. В 18268 строках не отмечается пропусков.
  • В таблице events неверный тип данных у колонки event_dt - строчный вместо даты. Так же отмечается наличие пропусков в колонке details - возможно там есть детали только для некоторых типов событий. Данные в этой колонке имеют тип значений с плавающей точкой.
  • В new_users также есть некорректный тип данный у колонки с датами. Пропусков не отмечается.
  • В marketing_events тоже проблема с типом данных для дат. Пропусков не отмечается.

Требуется провести следующую предобработку:

  • Для колонок с датами изменить тип данных
  • Проверить пропуски в events, есть ли возможность что-то с ними сделать
  • Подтвердить, что в остальных таблицах нет пропусков
  • Проверить наличие дубликатов.

Приступим к предобработке

Предобработка данных¶

К содержанию

Теперь приступим к выполнению пунктов предобработки, которые мы определили в предыдущем шаге.

Пропуски¶

К содержанию

In [4]:
display('Таблица participants',
        participants.isna().sum().to_frame(),
        'Таблица events',
        events.isna().sum().to_frame(),
        'Таблица new_users',
        new_users.isna().sum().to_frame(), 
        'Таблица marketing_events',
        marketing_events.isna().sum().to_frame())
'Таблица participants'
0
user_id 0
group 0
ab_test 0
'Таблица events'
0
user_id 0
event_dt 0
event_name 0
details 377577
'Таблица new_users'
0
user_id 0
first_date 0
region 0
device 0
'Таблица marketing_events'
0
name 0
regions 0
start_dt 0
finish_dt 0

Пропуски есть только в таблице new_users в колонке details. Возможно это связано с тем, что не для всех совершаемых событий есть какие-либо детали. Проверим эту теорию.

In [5]:
events.groupby('event_name')['details'].count().to_frame()
Out[5]:
details
event_name
login 0
product_cart 0
product_page 0
purchase 62740

Да, пропусков нет только у действия purchase, следовательно, это не аномалия и с этими пропусками не стоит ничего делать.

Дубликаты¶

К содержанию

Теперь проведём проверку на наличие дубликатов в датасетах. Для начала проверим полные явные дубликаты.

In [6]:
print('Количество явных дубликатов в таблице participants:', 
      participants.duplicated().sum(),
      '\nКоличество явных дубликатов в таблице events:',
      events.duplicated().sum(),
      '\nКоличество явных дубликатов в таблице new_users:', 
      new_users.duplicated().sum(),
      '\nКоличество явных дубликатов в таблице marketing_events:',
      marketing_events.duplicated().sum())
Количество явных дубликатов в таблице participants: 0 
Количество явных дубликатов в таблице events: 0 
Количество явных дубликатов в таблице new_users: 0 
Количество явных дубликатов в таблице marketing_events: 0

Полных дубликатов нет. Проверим, есть ли неполные дубликаты в таблице patricipants, что позволит нам сразу иметь представление о наличии пересечений пользователей между тестами и группами.

In [7]:
print('Пользователей, участвовавших более чем в 1 тесте или более чем в 1 группе:',
      participants['user_id'].duplicated().sum())
Пользователей, участвовавших более чем в 1 тесте или более чем в 1 группе: 1602

Почти 9 процентов пользователей участвуют в двух тестах и/или группах, чуть позже мы с ними разберемся. Посмотрим есть ли задвоение пользователей в разрезе стран/устройств.

In [8]:
print('Пользователей, заходивших из разных стран, или с разных устройств:',
      new_users['user_id'].duplicated().sum())
Пользователей, заходивших из разных стран, или с разных устройств: 0

Здесь нет повторяющихся значений user_id, теперь перейдем к проверки наличия неявных дубликатов : посмотрим, есть ли ошибки и нестыковки во всех категориальных данных.

In [9]:
print('Уникальные тесты:', participants['ab_test'].unique(),
      '\nУникальные действия:', events['event_name'].unique(),
      '\nУникальные регионы:', new_users['region'].unique(),
      '\nУникальные устройства:', new_users['device'].unique())
Уникальные тесты: ['recommender_system_test' 'interface_eu_test'] 
Уникальные действия: ['purchase' 'product_cart' 'product_page' 'login'] 
Уникальные регионы: ['EU' 'N.America' 'APAC' 'CIS'] 
Уникальные устройства: ['PC' 'Android' 'iPhone' 'Mac']

Категориальные данные также не повторяются и не имеют разночтений. Теперь приступим к изменению типов данных.

Типы данных¶

К содержанию

In [10]:
events['event_dt'] = pd.to_datetime(events['event_dt'])
new_users['first_date'] = pd.to_datetime(new_users['first_date'])
marketing_events['start_dt'] = pd.to_datetime(marketing_events['start_dt'])
marketing_events['finish_dt'] = pd.to_datetime(marketing_events['finish_dt'])
events.dtypes, new_users.dtypes, marketing_events.dtypes
Out[10]:
(user_id               object
 event_dt      datetime64[ns]
 event_name            object
 details              float64
 dtype: object,
 user_id               object
 first_date    datetime64[ns]
 region                object
 device                object
 dtype: object,
 name                 object
 regions              object
 start_dt     datetime64[ns]
 finish_dt    datetime64[ns]
 dtype: object)

Все типы данных успешно заменены.

Оценка корректности проведения теста¶

К содержанию

Теперь, когда мы провели предобработку, оценим, насколько корректно происходил набор групп и тестирование.

Есть ли пересечение с другими тестами¶

К содержанию

In [11]:
# Проверим пользователей, которые могли участвовать в двух или нескольких тестах одновременно:
cross_test_users = (participants.groupby('user_id').agg({'ab_test':'nunique'})
                    .query('ab_test > 1').reset_index())
conflict_test_list = cross_test_users['user_id'].to_list()

len(conflict_test_list)
Out[11]:
1602

Итак, как мы и обнаружили ранее - есть пользователи, которые встречаются в разных тестах. Избавимся от всех повторов и получим только тех пользователей, которые участвуют в интересующем нас тесте.

In [12]:
filtered_participants = participants.query('user_id not in @conflict_test_list')

test_participants = filtered_participants.query('ab_test == "recommender_system_test"')

Даты регистрации и совершения событий¶

К содержанию

In [13]:
print('Минимальная дата регистрации новых пользователей:', new_users['first_date'].min(),
      '\nМаксимальная дата регистрации новых пользователей:', new_users['first_date'].max())
Минимальная дата регистрации новых пользователей: 2020-12-07 00:00:00 
Максимальная дата регистрации новых пользователей: 2020-12-23 00:00:00

Первая дата регистрации совпадает с заявленными в техническом задании. А вот последняя дата выбивается. Отфильтруем пользователей, чтобы данные соответствовали заданию.

In [14]:
filtered_new_users = new_users.query('first_date < "2020-12-22"')
print('Максимальная дата регистрации новых пользователей:',
      filtered_new_users['first_date'].max())
Максимальная дата регистрации новых пользователей: 2020-12-21 00:00:00

Теперь проверим когда пользователи совершали события.

In [15]:
print('Дата первого события:', events['event_dt'].min(),
      '\nДата последнего события:', events['event_dt'].max())
Дата первого события: 2020-12-07 00:00:33 
Дата последнего события: 2020-12-30 23:36:33

Согласно нашим данным, тестирование проводилось на 5 дней меньше. Таким образом, не все пользователи имели возможность участвовать в эксперименте установленные 14 дней с момента регистрации. Это может привести к некоторому смещению результатов. С этим мы, увы, ничего сделать не можем.

Есть ли пересечения между группами¶

К содержанию

Теперь получим датасет с новыми пользователями, которые участвуют в нашем тесте, и заодно, проверим нет ли пересечения пользователей между группами.

In [16]:
target_test = filtered_new_users.merge(test_participants, on='user_id')
#Проверим, есть ли повторяющиеся пользователи в разных группах
target_test.groupby('user_id').agg({'group':'nunique'}).query('group > 1')
Out[16]:
group
user_id

Пересечений пользователей нет, теперь получим события, которые совершали все пользователи.

In [17]:
target_test = target_test.merge(events, on='user_id', how='left')
target_test.sample(20)
Out[17]:
user_id first_date region device group ab_test event_dt event_name details
17586 FD29640EA6992980 2020-12-19 EU PC A recommender_system_test 2020-12-19 19:31:53 product_page NaN
11314 E8173771F9C6C251 2020-12-16 EU PC A recommender_system_test 2020-12-16 22:36:05 login NaN
2231 B0B3A724D085010F 2020-12-14 EU Mac A recommender_system_test 2020-12-17 07:11:21 login NaN
15511 9EA3340B70A596AE 2020-12-18 EU Android A recommender_system_test 2020-12-27 04:14:07 login NaN
608 7E8720DB6A21CF66 2020-12-07 EU Android B recommender_system_test 2020-12-09 20:44:57 product_cart NaN
15995 20637F29BB029BEA 2020-12-18 EU Android A recommender_system_test 2020-12-18 06:36:49 login NaN
12091 4266741E592070B6 2020-12-16 EU iPhone A recommender_system_test 2020-12-16 07:05:00 login NaN
8201 92EFA54F7198BC87 2020-12-08 EU Android B recommender_system_test 2020-12-11 22:15:37 login NaN
8882 CBBD6A13EEE8DE81 2020-12-15 EU PC B recommender_system_test NaT NaN NaN
17727 A9DABAEA2233576C 2020-12-19 EU Android A recommender_system_test 2020-12-22 01:03:08 login NaN
866 6CAC7EB3574A1F02 2020-12-07 EU PC A recommender_system_test NaT NaN NaN
16234 F9D53F0BA957F728 2020-12-18 EU iPhone A recommender_system_test 2020-12-21 20:59:03 product_cart NaN
5753 4AEC1F638C2F1FC8 2020-12-21 EU PC A recommender_system_test 2020-12-29 21:29:15 product_page NaN
11869 1F77C3195B36A98B 2020-12-16 EU Android A recommender_system_test 2020-12-17 19:38:36 login NaN
11114 89EF9F0C1676188A 2020-12-16 EU Android B recommender_system_test 2020-12-18 16:42:17 login NaN
5 831887FE7F2D6CBA 2020-12-07 EU Android A recommender_system_test 2020-12-08 10:52:27 product_cart NaN
21058 5D7099BA15597D3D 2020-12-20 EU Android A recommender_system_test 2020-12-21 08:14:48 product_page NaN
15279 7347C03E6A300EFD 2020-12-18 EU Android A recommender_system_test 2020-12-25 02:45:27 purchase 4.99
16958 6E6DC58015E1CBA0 2020-12-12 EU PC A recommender_system_test NaT NaN NaN
19804 A354B804BD270C75 2020-12-20 EU iPhone A recommender_system_test 2020-12-24 19:36:30 purchase 4.99

Для удобства дальнейшей работы создадим столбец, содержащий только дату совершаемого события, без времени.

In [18]:
target_test.loc[:,'date'] = target_test.event_dt.apply(lambda x: x.date())
target_test
Out[18]:
user_id first_date region device group ab_test event_dt event_name details date
0 D72A72121175D8BE 2020-12-07 EU PC A recommender_system_test 2020-12-07 21:52:10 product_page NaN 2020-12-07
1 D72A72121175D8BE 2020-12-07 EU PC A recommender_system_test 2020-12-07 21:52:07 login NaN 2020-12-07
2 831887FE7F2D6CBA 2020-12-07 EU Android A recommender_system_test 2020-12-07 06:50:29 purchase 4.99 2020-12-07
3 831887FE7F2D6CBA 2020-12-07 EU Android A recommender_system_test 2020-12-09 02:19:17 purchase 99.99 2020-12-09
4 831887FE7F2D6CBA 2020-12-07 EU Android A recommender_system_test 2020-12-07 06:50:30 product_cart NaN 2020-12-07
... ... ... ... ... ... ... ... ... ... ...
21110 0416B34D35C8C8B8 2020-12-20 EU Android A recommender_system_test 2020-12-24 09:12:51 product_page NaN 2020-12-24
21111 0416B34D35C8C8B8 2020-12-20 EU Android A recommender_system_test 2020-12-20 20:58:25 login NaN 2020-12-20
21112 0416B34D35C8C8B8 2020-12-20 EU Android A recommender_system_test 2020-12-21 22:28:29 login NaN 2020-12-21
21113 0416B34D35C8C8B8 2020-12-20 EU Android A recommender_system_test 2020-12-24 09:12:49 login NaN 2020-12-24
21114 89CB0BFBC3F35126 2020-12-20 EU PC B recommender_system_test NaT NaN NaN NaT

21115 rows × 10 columns

По выведенным данным можно увидеть, что есть пользователи, которые не совершали действий.

Регион регистрации¶

К содержанию

Теперь посмотрим, соответствует ли количество новых пользователей из региона EU поставленному тех.заданию.

In [19]:
region_data = filtered_new_users.groupby('region').agg({'user_id':'nunique'})
region_data = region_data.merge(target_test.pivot_table(index='region',
                                                        values='user_id',
                                                        aggfunc = 'nunique'), on='region')
region_data.columns = ['all_users', 'test_users']
region_data['share'] = round(region_data['test_users']/region_data['all_users']*100)
region_data
Out[19]:
all_users test_users share
region
APAC 2883 72 2.0
CIS 2900 55 2.0
EU 42340 4749 11.0
N.America 8347 223 3.0

Набрать необходимый процент пользователей не удалось. Еще одно несоответствие.

Динамика набора пользователей в группы¶

К содержанию

Проверим, как набирались пользователи в группы.

In [20]:
plt.figure(figsize=(15, 7))
sns.lineplot(data=target_test.pivot_table(index='first_date',
                                          columns='group',
                                          values='user_id', 
                                          aggfunc='count'))
plt.xlabel('Дата регистрации')
plt.ylabel('Количество пользователей')
plt.title('Динамика набора пользователей в группы', fontsize=20, color='SteelBlue')
plt.show()

Пользователи набирались неравномерно. В группе А наблюдается спад в первые дни набора, а затем резкий скачок зарегистрировавшихся пользователей 14 декабря. Группа В также набиралась неравномерно, также заметно сильное различие в количестве пользователей.

In [21]:
target_test.groupby('group')['user_id'].nunique().plot.bar(rot=0,
                                                           figsize=(12,6), 
                                                           xlabel='Группа',
                                                           ylabel='Количество пользователей')
plt.title('Распределение пользователей по группам', fontsize=20, color='SteelBlue')
plt.show()

Да, в группах различается количество пользователей, и достаточно сильно. Скорее всего, это повлияет на результаты тестирования.

Пользовательская активность¶

К содержанию

Оценим, все ли пользователи совершали действия после регистрации.

In [22]:
only_registration = (target_test.groupby(['user_id','group']).agg({'event_name':'nunique'})
                     .query('event_name < 1').reset_index())
print('Общее число пользователей, участвующих в тесте:', target_test['user_id'].nunique(),
      '\nЧисло пользователей, которые не совершали действий:', len(only_registration))
only_registration_list = only_registration['user_id'].to_list()
Общее число пользователей, участвующих в тесте: 5099 
Число пользователей, которые не совершали действий: 2311

45% пользователей не совершали никаких действий после регистрации. Очень много, однако, такое тоже может быть. Оценим, равномерно ли распределены пользователи, которые не совершали действий после регистрации.

In [23]:
only_registration.groupby('group')['user_id'].count().plot.bar(color='g',
                                                               rot=0,
                                                               xlabel='Группа',
                                                               ylabel='Количество пользователей',
                                                               figsize=(12,6))
plt.title('Распределение пользователей,\n не совершавших действий по группам', 
          fontsize=20, color='SteelBlue')
plt.show()

Более половины пользователей, которые не совершали события находятся в группе В. В контрольной группе только треть таких пользователей.

Так как мы не сможем корректно оценить изменения конверсии на пользователях, которые не совершали события - отфильтруем наш датафрейм. Это не приведет к серьезному сдвигу, так как нас не интересует изменение конверсии пользователей в авторизацию. А также это повысит точность оценки изменений интересующих нас метрик.

In [24]:
f_target_test = target_test.query('user_id not in @only_registration_list')
f_target_test.groupby('group')['user_id'].nunique().plot.bar(rot=0,
                                                           figsize=(12,6),
                                                           color = 'orange',
                                                           xlabel='Группа',
                                                           ylabel='Количество пользователей')
plt.title('Распределение пользователей по группам после фильтрации',
          fontsize=20, color='SteelBlue')
plt.show()

Теперь можно заметить более выраженную неравномерность в наборе групп.

Теперь посмотрим, в какой день после регистрации пользователи в среднем начинали совершать действия.

In [25]:
#Добавим разницу между датой регистрации и датой совершения событий
f_target_test['days_since_registration'] = (f_target_test['event_dt'] - 
                                            f_target_test['first_date']).dt.days
#Теперь отфильтруем так, чтобы были только те события, 
#которые не совершались более чем через 14 дней
lifetime_dataset_filtered = f_target_test[f_target_test['days_since_registration'] < 14]
#Получим распределение совершения событий по дням с момента регистрации в разрезе групп
lifetime_event = lifetime_dataset_filtered.pivot_table(index='days_since_registration',
                                                       columns = 'group',
                                                       values='user_id', 
                                                       aggfunc='count')

lifetime_event.plot.bar(rot=0, xlabel='День с момента регистрации', 
                        ylabel='Количество пользователей', figsize=(15,8))
plt.title('Распределение совершения событий пользователями по дням после регистрации', 
          fontsize=20, color='SteelBlue')
plt.show()

В основном, в обеих группах чаще всего пользователи совершали события в течение первого дня, с момента регистрации (в день регистрации). Затем, к 14 дню все меньше пользователей совершают события.

Теперь посмотрим, когда пользователи совершают впервые каждое событие.

In [26]:
first_event_dates = (lifetime_dataset_filtered
                     .groupby(['user_id','event_name'])['days_since_registration'].min()
                     .to_frame().reset_index())
first_dates = first_event_dates.pivot_table(index='event_name',
                                            columns='days_since_registration',
                                            values='user_id',aggfunc='count')
first_dates = first_dates.T
first_dates.plot.bar(stacked=True, figsize=(15,8), rot=0)
plt.xlabel('День с момента регистрации')
plt.ylabel('Количество событий')
plt.title('Время совершения пользователем каждого вида события в первый раз',
          fontsize=20, color='SteelBlue')
plt.show()

Все события в первый раз в основном совершаются в первый день - в день регистрации. Некоторые пользователи авторизуются только на второй день, перейти в корзину - на третий. Совершить оплату могут как в первые три дня, так и на 7 день после регистрации.

Вывод¶

К содержанию

Проведя изучение данных, вот что можно сказать о соответствии проводимого теста поставленному ТЗ(тех.заданию):

  • В выборке встречались пользователи, которые участвовали в обоих тестах,
  • Регистрация пользователей продолжалась дольше указанной даты,
  • Тестирование закончилось раньше указанного времени на 5 дней,
  • Пользователи в группы набирались неравномерно, в контрольной группе пользователей больше, чем в тестируемой. Согласно поставленной задаче - прирост конверсии не менее чем на 5 процентных пунктов при базовой конверсии в 50%, размер каждой группы должен был быть не менее 1567 пользователей (согласно калькулятору). В нашем случае, после фильтрации данных, в тестируемой группе остается менее 1000 пользователей, и становится заметным сильный дисбаланс набора групп;
  • Не достигнута ожидаемая доля в 15% от общего количества участников из целевого региона,
  • Около половины пользователей только регистрируются и не совершают действий, больше всего таких пользователей оказалось в тестовой группе.

Таким образом, выборка составлена некорректно, возможность получения достоверного результата минимальна.

Исследовательский анализ данных¶

К содержанию

Теперь проведём исследовательский анализ данных, чтобы сравнить изменения пользовательской активности в разных группах.

Распределение количества событий на пользователя в разрезе групп¶

К содержанию

In [27]:
not_null_events = f_target_test.query('user_id not in @only_registration_list')
event_per_users = not_null_events.pivot_table(index='user_id',
                                              values=['event_name','group'],
                                              aggfunc={'event_name':'count',
                                                       'group':'first'}).reset_index()
plt.figure(figsize=(15,8))
sns.histplot(event_per_users, x='event_name', hue='group', bins=25, kde=True)
plt.xlabel('Среднеее число событий')
plt.ylabel('Количество пользователей')
plt.title('Среднее число событий, совершаемых пользователем', fontsize=20, color='SteelBlue')
plt.show()

В обеих группах каждый пользователь в среднем совершал менее 10 событий. Чаще всего пользователи совершали около 5 событий.

Динамика количества событий в группах по дням¶

К содержанию

In [28]:
event_group_a = f_target_test.query('group == "A"').pivot_table(index='date',
                                                              columns='event_name', 
                                                              values='user_id',
                                                              aggfunc='count')
event_group_a.columns = ['login','product_cart','product_page','purchase']
event_group_b = f_target_test.query('group == "B"').pivot_table(index='date',
                                                              columns='event_name', 
                                                              values='user_id',
                                                              aggfunc='count')
event_group_b.columns = ['login','product_cart','product_page','purchase']

fig, axes = plt.subplots(2, figsize=(15, 18))

event_group_a.plot.bar(stacked=True,
                       ax=axes[0], 
                       xlabel='Дата', 
                       ylabel = 'Количество событий', 
                       title='В группе А')
plt.setp(axes[0].xaxis.get_majorticklabels(), rotation=30 , ha="right")
event_group_b.plot.bar(stacked=True, 
                       ax=axes[1], 
                       xlabel='Дата', 
                       ylabel = 'Количество событий', 
                       title='В группе В')
plt.setp(axes[1].xaxis.get_majorticklabels(), rotation=30 , ha="right")
fig.suptitle('Динамика количества событий в группах по дням в разрезе событий', 
             fontsize=20, color='SteelBlue')
fig.show()
  • В группе А, как и в группе В наблюдается скачок в совершении событий 21 декабря 2020 года. Это может быть обосновано тем, что 25 декабря празднуется католическое рождество - важный государственный праздник для нашей целевой страны.
  • В группе В также отмечаются колебания по дням недели - всплески по понедельникам и средам.
  • После 21 декабря совершение действий в обеих группах идет на спад, что также может объясняться приближением праздника.

Проверка влияния маркетинговых мероприятий¶

К содержанию

Посмотрим, могли ли проводимые маркетинговые события каким-либо образом повлиять на поведение пользователей тестируемых групп.

In [29]:
#Выведем все маркетинговые мероприятия, которые начинаются в исследуемые даты
sale_events = (marketing_events
               .query('start_dt >="2020-12-07" & start_dt <"2020-12-30"')
               .reset_index(drop=True))
sale_events
Out[29]:
name regions start_dt finish_dt
0 Christmas&New Year Promo EU, N.America 2020-12-25 2021-01-03

У нас есть одно мероприятие, которое накладывается на время проведения теста и может повлиять на нашу целевую аудиторию. Проверим, так ли это.

In [30]:
plt.figure(figsize=(15,5))
plt.title('Влияние маркетинговых событий на активность пользователей',
          fontsize=20, color='SteelBlue')

ax = sns.lineplot(data=f_target_test.pivot_table(index='date',
                                               columns='group', 
                                               values='event_name', 
                                               aggfunc='count'))
ax.set_xlabel('Дата')
ax.set_ylabel('Количество событий')
#Выведем цветные прямоугольники на график для визуализации маркетинговых мероприятий
#Получим палетку цветов, чтобы отличать события
colors = sns.color_palette('Accent')
#Автоматизируем визуализацию
for index, row in sale_events.iterrows():
    color = colors[index % len(colors)]
    ax.axvspan(xmin=row['start_dt'], 
               xmax=row['finish_dt'], 
               alpha=0.3, label=row['name'], color = color)
#Выведем легенду, которая принимает цвет события и группы, 
#и сдвинем, чтобы она не мешала в случае появления новых мероприятий
plt.legend(loc='best',bbox_to_anchor = (1,1))
plt.show()

Судя по графику, в обеих группах наблюдается спад пользовательской активности после 21 декабря. Проведение маркетингового события мало отразилось на пользователях обеих групп, поэтому не будем избавляться от этих данных.

Продуктовая воронка¶

К содержанию

Теперь перейдем к самому важному - узнаем, получилось ли достичь указанного увеличения конверсии в тестовой группе. У нас есть последовательные действия, которые могут сделать пользователи, которые складываются в следующую воронку:

  1. login - авторизоваться в приложении,
  2. product_page - перейти на страницу товара,
  3. product_cart - перейти в корзину,
  4. purchase - совершить покупку.

Проверим увеличение конверсии в группе В по следующим действиям : product_page,product_cart,purchase.

In [31]:
test_funnel = f_target_test.pivot_table(index='group',
                                        columns = 'event_name', 
                                        values = 'user_id', 
                                        aggfunc = 'nunique').T
test_funnel.columns = ['A','B']
test_funnel = test_funnel.sort_values(by='A',ascending=False).reset_index()
test_funnel = test_funnel.reindex([0,1,3,2])
test_funnel
Out[31]:
event_name A B
0 login 2082 706
1 product_page 1360 397
3 product_cart 631 195
2 purchase 652 198
In [32]:
fig = go.Figure()

fig.add_trace(go.Funnel(
    name = 'Группа А',
    y = test_funnel['event_name'],
    x = test_funnel['A'],
    textinfo = "value+percent previous"))

fig.add_trace(go.Funnel(
    name = 'Группа В',
    orientation = "h",
    y = test_funnel['event_name'],
    x = test_funnel['B'],
    textposition = "inside",
    textinfo = "value+percent previous"))

fig.update_layout(
    title_text='Воронка по группам теста',
    width = 1000,
    height = 500
)

fig.show()

Нет, достичь увеличения конверсии не удалось, наоборот, отмечается снижение. Также, можно отметить, что не все пользователи выполняли каждое действие из указанной воронки - некоторые пользователи пропускают посещение корзины и оплачивают товары сразу.

Вывод¶

К содержанию

На основании исследовательского анализа мы можем сделать следующие выводы:

  • В группе В не достигнуто увеличение конверсии по основным метрикам, отмечатеся снижение, по сравнению с контрольной группой.
  • В группе В отмечаются колебания в совершении действий в зависимости от дня недели - больше всего действий совершается в начале недели, есть всплески активности в понедельник и среду.
  • На обе группы оказало влияние приближение праздника - Наблюдается резкое повышение активности вплоть до пика 21 декабря, затем спад активности.
  • В группе В пользователи совершали меньше действий (возможно это связано с меньшим числом пользователей, чем в контрольной группе.

Оценка результатов А/В тестирования¶

К содержанию

Настало время оценить результаты А/В тестирования и сделать вывод о проведенном тесте.

Сравнение долей¶

К содержанию

Сравним конверсию по основным метрикам для обеих групп и оценим статистическую значимость различий, если таковые имеются.

Для сравнения выведем следующие гипотезы: Для всех тестирований ниже примем следующие гипотезы:

  • Н0: Среднее количество пользователей совершивших событие в группах А и В равно
  • Н1: Среднее количество пользователей совершивших событие в группах А и В не равно
In [33]:
event_by_groups = f_target_test.pivot_table(index='event_name',
                                            values='user_id',
                                            columns='group', 
                                            aggfunc='nunique')
event_by_groups = event_by_groups.T
event_by_groups['total_users'] = f_target_test.groupby('group').agg({'user_id':'nunique'})
event_by_groups.columns = ['login',
                           'product_cart',
                           'product_page',
                           'purchase','total_users']
event_by_groups = event_by_groups.reset_index()
event_by_groups
Out[33]:
group login product_cart product_page purchase total_users
0 A 2082 631 1360 652 2082
1 B 706 195 397 198 706
In [34]:
#Функция, которая проводит z-тестирование
def z_test(hits, trials, alpha):
    # доля успехов в исследуемых группах:
    p1 = hits[0]/trials[0]
    p2 = hits[1]/trials[1]

    # доля успехов в комбинированном датасете:
    p_combined = (hits[0] + hits[1]) / (trials[0] + trials[1])

    # разница долей в датасетах
    difference = p1 - p2 

    # считаем статистику в ст.отклонениях стандартного нормального распределения
    z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/trials[0] + 1/trials[1]))

    # задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
    distr = st.norm(0, 1)  

    p_value = (1 - distr.cdf(abs(z_value))) * 2

    print('p-значение: ', p_value)

    if p_value < alpha:
        print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
    else:
        print('Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными') 
    #вернем значение p_value для поправок на множественное тестирование
    return p_value
In [35]:
#Создадим функцию, которая будет проводить z-тест для всех параметров по группам
#На вход будет принимать номера экспериментов и значение статистической значимости
def lazy_check (n_1, n_2, alpha):
    #Зададим переменные, которые будут принимать значения p_value для поправок на множественное
    #проведение гипотез
    pv1 = 0
    pv2 = 0
    pv3 = 0

    #Зададим список значений trials для обоих экспериментов 
    #(общее количество пользователей каждого эксперимента)
    n_trials = ([event_by_groups['total_users'][n_1],
                 event_by_groups['total_users'][n_2]])
    #Теперь получим списки значений hits по каждой исследуемой выборке
   
    product_page = ([event_by_groups['product_page'][n_1],
                     event_by_groups['product_page'][n_2]])
    
    product_cart = ([event_by_groups['product_cart'][n_1], 
                     event_by_groups['product_cart'][n_2]])
    
    purchase = ([event_by_groups['purchase'][n_1], 
                 event_by_groups['purchase'][n_2]])
    
    #Автоматизируем вывод результатов тестирования
    print('')
    print('Сравнение долей по пользователям, открывшим страницу товара:')
    pv1 = z_test(product_page, n_trials, alpha)
    print('')
    print('Сравнение долей по пользователям, перешедшим в корзину:')
    pv1 = z_test(product_cart, n_trials, alpha)
    print('')
    print('Сравнение долей по пользователям, совершившим покупку:')
    pv3 = z_test(purchase, n_trials, alpha)
    #Вернем список p_value
    return [pv1, pv2, pv3]

Для начала проведём исследование, при вероятности ложноположительного результата в 5%.

In [36]:
pv_05 = 0
pv_05 = lazy_check(0,1, 0.05)
Сравнение долей по пользователям, открывшим страницу товара:
p-значение:  1.5371909704686715e-05
Отвергаем нулевую гипотезу: между долями есть значимая разница

Сравнение долей по пользователям, перешедшим в корзину:
p-значение:  0.1766337419130104
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Сравнение долей по пользователям, совершившим покупку:
p-значение:  0.10281767567786759
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Самым критическим оказалась разница в конверсии в открытие страницы товара. Похоже, что рекомендации несколько отталкивают пользователей. Однако, это никак не повлияло на добавление товаров в корзину и покупку товаров.

Теперь оценим разницу, при вероятности ложноположительного результата в 10%.

In [37]:
pv_10 = 0
pv_10 = lazy_check(0,1,0.1)
Сравнение долей по пользователям, открывшим страницу товара:
p-значение:  1.5371909704686715e-05
Отвергаем нулевую гипотезу: между долями есть значимая разница

Сравнение долей по пользователям, перешедшим в корзину:
p-значение:  0.1766337419130104
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

Сравнение долей по пользователям, совершившим покупку:
p-значение:  0.10281767567786759
Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными

При увеличении вероятности ложноположительного результата также есть разница только в конверсии в просмотр страницы продукта. Остальные метрики между группами не различаются.

Поправка на множественное тестирование¶

К содержанию

Так как мы сравниваем доли по нескольким изменениям (в нашем случае доли пользователей, которые побывали на конкретном этапе), стоит учесть поправки на множественное тестирование. Таким образом, мы сможем избежать возникновения ошибок и скорректировать уровень статистической значимости. Для этого мы проверим наши проведенные тесты на два типа ошибок:

FWER (Family-Wise Error Rate) - Групповая вероятность ошибки, которая представляет собой вероятность получить по крайней мере одну ошибку первого рода FDR (False Discovery Rate) — это среднее значение отношения ошибок первого рода к общему количеству отклонений основной гипотезы

In [38]:
lpv_05 = [pv_05]
lpv_10 = [pv_10]
#Вероятность получить хотя бы одну ошибку первого рода для двух значений alpha
print("FWER: " + str(multipletests(sorted(pv_05), alpha=0.05, 
                     method='holm', is_sorted = True))) 
print("FWER: " + str(multipletests(sorted(pv_10), alpha=0.1, 
                     method='holm', is_sorted = True))) 
print('')

#Среднее значение отношения ошибок первого рода к общему количеству отклонений основной гипотезы
print("FDR: " + str(multipletests(pv_05, alpha=0.05, 
                    method='fdr_bh', is_sorted = False))) 
print("FDR: " + str(multipletests(pv_10, alpha=0.1, 
                    method='fdr_bh', is_sorted = False)))
FWER: (array([ True, False, False]), array([0.        , 0.20563535, 0.20563535]), 0.016952427508441503, 0.016666666666666666)
FWER: (array([ True, False, False]), array([0.        , 0.20563535, 0.20563535]), 0.03451061539437028, 0.03333333333333333)

FDR: (array([False,  True, False]), array([0.17663374, 0.        , 0.15422651]), 0.016952427508441503, 0.016666666666666666)
FDR: (array([False,  True, False]), array([0.17663374, 0.        , 0.15422651]), 0.03451061539437028, 0.03333333333333333)

Использованный метод выводит результат проведения теста в виде буллевых значений, где:

  • True - нулевая гипотеза отвергается,
  • False - нулевая гипотеза не отвергается;

Затем выводится список скорректированных значений p_value для каждого проведенного теста, а также скорректированные значения для уровня статистической значимости по двум методам поправок - Сидака и Бонферрони.

Исходя из полученных нами данных, при снижении вероятности возникновения ложноположительного результата до 3% разница в переходе на страницу товара пользователями из двух групп теряет статистическую значимость, а вот разница перехода в корзину наоборот становится статистически значимой. Разницы между группами в совершении покупок все так же не наблюдается.

Вывод¶

К содержанию

  • При проведении теста обнаружено, что конверсия пользователей контрольной группы выше при переходе на страницу товара (в корзину - при использовании поправок на множественное тестирование) и эта разница статистически значима.
  • В остальных действиях конверсия пользователей не отличается.
  • Достигнуть ожидаемого эффекта в изменении конверсии не удалось.

Однако, тест не показал значимых ухудшений.

Выводы¶

К содержанию

Мы провели большую работу по оценке результатов проведенного А/В тестирования и вот какие выводы мы можем сделать:

  1. О корректности теста:

Обнаружено несоответствие поставленному ТЗ:

  • Присутствовали пользователи, которые участовали в двух разных тестах,
  • Тест не проведен до конца - нет данных за оставшиеся 5 дней,
  • Группы собраны неравномерно, набрано недостаточно пользователей в тестовой группе для анализа,
  • Присутствовали пользователи, которые регистрировались позже установленной даты,
  • Доля целевых пользователей не соответствовала требуемой, в частности - была меньше чем 15%.
  • Чуть меньше половины пользователей не совершали никаких действий, что могло искажать результаты.
  1. Об изменениях пользовательской активности:

    • В тестовой группе пользователи совершали меньше действий, по сравнению с контрольной - это может быть обосновано меньшим количеством пользователей,
    • В тестовой группе наблюдаются недельные колебания активности пользователей - в начале недели совершается больше действий,
    • На пользовательскую активность обеих групп повлияло проведение католического Рождества - всплеск активности 21 декабря и затухание после.
    • В среднем пользователи обеих групп совершали менее 10 действий,
    • Активность пользователей плавно снижалась с начала регистрации к концу второй недели,
    • В среднем, большинство пользователей совершали свое первое действие каждого типа на первый день,
    • Пользователи тестовой группы показали снижение конверсии
  2. О результатах теста:

    • При наблюдаемом снижении конверсии, статистическая значимость наблюдается только для разницы в переходе на страницу продукта (в корзину, при внесении поправок).
    • Достигнуть ожидаемого эффекта не удалось.

Какие рекомендации можно дать:

  • Следует прервать тест, по следующим причинам:
    • несоответствие поставленным условиям,
    • влияние внешних факторов на пользовательскую активность (государственный праздник),
    • наличие проблемы подглядывания,
    • неполные данные,
    • неравномерность набора пользователей
  • Тест не показал достижения ожидаемого эффекта, однако внесенные изменения не стоит считать ухудшающими, скорее не оказывающими эффекта.
  • Следует пересмотреть интересующую воронку событий, чтобы избежать "перескакивания" пользователями некоторых стадий.